iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Mobile Development

Jetpack Compose 從心開始系列 第 15

Jetpack Compose 從心開始 Day15 - 協同程式

  • 分享至 

  • xImage
  •  

前言

    Compose 使用 Kotlin 語言的宣告式語法,讓開發者能夠以更直觀、更簡潔的方式來構建使用者介面。
    Kotlin 協同程式,也是很重要的概念。

Kotlin 協同程式

提供了一種非同步且輕量級的方式來處理長時間運行的任務,例如網路請求、資料庫操作等,而不阻塞主執行緒。

延遲

在主程式直接delay

import kotlinx.coroutines.*

fun main() {
   println("iThome鐵人賽")
   delay(1000)
   println("30天")
}

就會噴 error ,就鐵人賽delay 1天,就斷賽一樣

Suspend function 'delay' should be called only from a coroutine or another suspend function

使用 runBlocking:

阻塞主執行緒,等待協同程式完成

  • 用於在測試或簡單的應用程式中阻塞主執行緒,直到協同程式執行完畢。
  • 不建議在 UI 線程中使用,因為會導致 UI 凍結。
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("iThome鐵人賽")
        delay(1000)
        println("30天")
    }
}

iThome鐵人賽
30天

suspend function

  • 宣告一個函式可以暫停執行,讓出執行緒,直到有結果返回或被取消。
  • 只能在協同程式中呼叫懸掛函式。
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("iThome鐵人賽")
        printCompetitionDays()
    }
}

suspend fun printCompetitionDays() {
    delay(1000)
    println("30天")
}

iThome鐵人賽
30天

查看延遲執行這個程式需要多長時間

import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("iThome鐵人賽")
            printCompetitionDays()
            printCompetitionCategory()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printCompetitionDays() {
    delay(1000)
    println("30天")
}

suspend fun printCompetitionCategory() {
    delay(1000)
    println("行動開發")
}

iThome鐵人賽
30天
行動開發
執行時間: 2.089 秒

非同步程式碼

Kotlin 中的協同程式遵循稱為結構化並行的重要概念

launch:

啟動一個新的協同程式
使用並行方式執行任務,請在程式碼中新增多個 launch() 函式,以便同時處理多個協同程式。

import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("iThome鐵人賽")
            launch {
                printCompetitionDays() 
            }
            launch {
                printCompetitionCategory() 
            }
        }
    println("Execution time: ${time / 1000.0} seconds")    
    }
}

suspend fun printCompetitionDays() {
    delay(1000)
    println("30天")
}

suspend fun printCompetitionCategory() {
    delay(1000)
    println("行動開發")
}

iThome鐵人賽
30天
行動開發
執行時間: 1.103 秒

執行時間: 2.089 秒  ->  執行時間: 1.103 秒

執行程式順序

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("iThome鐵人賽")
            launch {
                printCompetitionDays() 
            }
            launch {
                printCompetitionCategory() 
            }
            println("祝你堅持的三十天!")
        }
    println("Execution time: ${time / 1000.0} seconds")    
    }
}

....

iThome鐵人賽
祝你堅持的三十天!
30天
行動開發
執行時間: 1.098 秒

會發現為 printCompetitionDays() 和 printCompetitionCategory()  啟動兩個新的協同程式後,就可以繼續處理輸出 祝你堅持的三十天! 的下一個指示了。

async()函數

用於啟動一個新的協同程式,並 返回一個 Deferred 物件。這個 Deferred 物件代表著這個協同程式的未來結果。我們可以在需要的時候,透過 await() 函式來取得這個結果。
相較於 launch(),async() 更常用於需要取得回傳值的非同步任務。

fun main() {
    runBlocking {
        println("iThome鐵人賽")
        val days: Deferred<String> = async {
            	getCompetitionDays()
        	}
        val category: Deferred<String> = async {
            	getCompetitionCategory()
        }
        println("${days.await()} ${category.await()}")
        println("祝你堅持的三十天!")
    }
}

suspend fun getCompetitionDays(): String {
    delay(1000)
    return "30天"
}

suspend fun getCompetitionCategory(): String {
    delay(1000)
    return "行動開發"
}

iThome鐵人賽
30天 行動開發
祝你堅持的三十天!

平行分解

平行分解 (Parallel Decomposition) 則是將一個任務拆分成多個子任務,並同時執行這些子任務,以提高效率。在 Kotlin 協同程式中,我們可以利用 async 函式和 CoroutineScope 來輕鬆實現平行分解。

coroutineScope

  • 協同程式的範圍: 定義了一個協同程式的生命週期。
  • 取消協同程式: 當 CoroutineScope 被取消時,其下的所有協同程式也會被取消。
  • 提供上下文: 提供協同程式執行所需的上下文資訊,例如 Dispatchers。
fun main() {
    runBlocking {
        println("iThome鐵人賽")
        println(getCompetitionReport())
        println("祝你堅持的三十天!")
    }
}

suspend fun getCompetitionReport() = coroutineScope {
    val category = async { getCompetitionCategory() }
    val days = async { getCompetitionDays() }
    
    
    "${category.await()} ${days.await()}"
}

suspend fun getCompetitionDays(): String {
    delay(1000)
    return "30天"
}

suspend fun getCompetitionCategory(): String {
    delay(1000)
    return "行動開發"
}

iThome鐵人賽
行動開發 30天
祝你堅持的三十天!

例外狀況

try-catch 例外狀況

如果您知道程式碼的某些部分可能會擲回例外狀況,可以使用 try-catch 區塊包住該程式碼。

有Exception
iThome鐵人賽
Exception in thread "main" java.lang.AssertionError: 忘記寫文章斷賽了
 at FileKt.getCompetitionDays (File.kt:61) 
 at FileKt$getCompetitionDays$1.invokeSuspend (File.kt:-1) 
 at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33) 

解決

fun main() {
    runBlocking {
        println("iThome鐵人賽")
        println(getCompetitionReport())
        println("祝你堅持的三十天!")
    }
}

suspend fun getCompetitionReport() = coroutineScope {
    val category = async { getCompetitionCategory() }
    val days = async { 
        try {
        	getCompetitionDays()
        } catch (e: AssertionError) {
            println("斷賽原因 exception $e")
            "{ 沒po文 斷賽 }"
        }    
    }
    
    "${category.await()} ${days.await()}"
}

suspend fun getCompetitionDays(): String {
    delay(500)
    throw AssertionError("忘記寫文章斷賽了")
    return "30天"
}

suspend fun getCompetitionCategory(): String {
    delay(1000)
    return "行動開發"
}

iThome鐵人賽
斷賽原因 exception java.lang.AssertionError: 忘記寫文章斷賽了
行動開發 { 沒po文 斷賽 }
祝你堅持的三十天!

取消

為什麼要取消協同程式?

  • 資源釋放: 當協同程式不再需要時,及時取消可以釋放系統資源。
  • 錯誤處理: 當發生錯誤時,可以取消相關的協同程式。
  • 使用者互動: 根據使用者操作,可以取消正在進行的任務。
fun main() {
    runBlocking {
        println("iThome鐵人賽")
        println(getCompetitionReport())
        println("祝你堅持的三十天!")
    }
}

suspend fun getCompetitionReport() = coroutineScope {
    val category = async { getCompetitionCategory() }
    val days = async { getCompetitionDays() }
    
    delay(200)
    days.cancel() //決定棄賽
    
    "${category.await()} }"
}

suspend fun getCompetitionDays(): String {
    delay(500)
    return "30天"
}

suspend fun getCompetitionCategory(): String {
    delay(1000)
    return "行動開發"
}

iThome鐵人賽
行動開發
祝你堅持的三十天!

job

代表一個協同程式
可以用來控制協同程式的生命週期,例如啟動、取消、等待完成等。
val job = launch { ... }
job.cancel()

https://ithelp.ithome.com.tw/upload/images/20240925/20121643sdHXd6xy9f.png

父項/子項關係會為子項、父項以及屬於同一父項的其他子項指定特定行為,因此十分重要。在前面的例子中,我們已透過天氣程式介紹這個行為。

  • 如果父項工作遭到取消,則子項工作也會取消。
  • 使用 job.cancel() 取消子項工作時,子項工作會終止,但不會取消其父項工作。
  • 如果工作因出現例外狀況而失敗,其父項也會因為該例外狀況而遭取消。這就是所謂的錯誤向上蔓延 (蔓延至父項、父項的父項等,依此類推)。

CoroutineContext 是什麼

CoroutineContext 可以被視為協同程式的上下文環境,它包含了協同程式執行所需要的一些資訊,例如:

  • Job: 代表協同程式本身,用於控制協同程式的生命週期(啟動、取消、等待完成)。
  • Dispatcher: 指定協同程式執行的線程,如 Dispatchers.Main、Dispatchers.IO、Dispatchers.Default。
  • CoroutineName: 為協同程式指定一個名稱,方便調試。

Dispatcher 調度工具

  • Dispatchers.Main:使用這個調派程式在 Android 主執行緒上執行協同程式。這個調度工具主要用於處理 UI 更新和互動,以及執行快速作業。
  • Dispatchers.IO:這個調派程式已完成最佳化調整,以便在主執行緒外執行磁碟或網路 I/O。例如讀取或寫入檔案,以及執行任何網路作業。
  • Dispatchers.Default:如果未在結構定義中指定調度工具,系統在呼叫 launch() 和 async() 時使用此預設調度工具。您可以使用這個調度器,在主執行緒外執行計算密集型作業。例如,處理點陣圖圖片檔。
import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    val job = scope.launch {
        try {
            delay(500L)
            println("Task started")
            delay(1500L)
            println("Task finished")
        } catch (e: CancellationException) {
            println("Task was cancelled")
        } finally {
            println("Cleaning up")
        }
    }

    delay(1300L)
    job.cancelAndJoin()
    println("Main program finished")
}

Task started
Task was cancelled
Cleaning up
Main program finished

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("${Thread.currentThread().name} - runBlocking function")
                launch {
            println("${Thread.currentThread().name} - launch function")
            withContext(Dispatchers.Default) {
                println("${Thread.currentThread().name} - withContext function")
                delay(1000)
                println("10 results found.")
            }
            println("${Thread.currentThread().name} - end of launch function")
        }
        println("Loading...")
    }
}

main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function


上一篇
Jetpack Compose 從心開始 Day14 - Navigation 導覽
下一篇
Jetpack Compose 從心開始 Day16 - Android 中的協同程式
系列文
Jetpack Compose 從心開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言